using System;
using System.Text;
using System.IO;
using System.Net;
using System.Diagnostics;


namespace Gigya.Socialize.SDK
{
    /// <summary>
    /// A Request to Gigya Socialize API 
    /// </summary>
    /// <remarks>Author: Tamir Korem. Updated by: Yaron Thurm</remarks>
    public class GSRequest
    {
        #region Private Members
        private string domain;
        private string path;
        private string method;
        private string apiKey; // For OAuth1
        private string secretKey; // For OAuth1
        private string accessToken; // For OAuth2
        private GSDictionary dictionaryParams;
        private bool useHTTPS;
        private string format;
        private StringBuilder traceLog = new StringBuilder();
        private static string unreservedCharsstring = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~";
        private static char[] unreservedChars;
        #endregion

        
        #region Contructors

        /// <summary>
        /// Static constructor
        /// </summary>
        static GSRequest()
        {
            unreservedChars = unreservedCharsstring.ToCharArray();
            Array.Sort(unreservedChars);
        }

        /// <summary>
        /// Constructs a request using an apiKey and secretKey.
        /// Suitable for calling our old REST API
        /// </summary>
        /// <param name="apiKey">Gigya's API key obtained from Site-Setup page on the Gigya website</param>
        /// <param name="secretKey">Secret Key obtained from Site-Setup page on the Gigya website</param>
        /// <param name="apiMethod">The API method (including namespace) to call. For example: socialize.getUserInfo
        /// If namespaces is not supplied "socialize" is assumed</param>
        /// <param name="currDictionaryParams">The request parameters</param>
        /// <param name="useHTTPS">Set this to true if you want to use HTTPS.
        /// The library uses HTTP by default (the request is signed with the secret key) 
        /// but you can use this parameter to override the default</param>
        public GSRequest(string apiKey, string secretKey, string apiMethod, GSDictionary currDictionaryParams, bool useHTTPS)
        {
            if (apiMethod == null || apiMethod == "")
                return;

            if (apiMethod.StartsWith("/"))
                apiMethod = apiMethod.Substring("/".Length);

            if (apiMethod.IndexOf(".") == -1)
            {
                this.domain = "socialize.gigya.com";
                this.path = "/socialize." + apiMethod;
            }
            else
            {
                string[] tokens = apiMethod.Split(new char[] { '\\', '.' });
                this.domain = tokens[0] + ".gigya.com";
                this.path = "/" + apiMethod;
            }

            this.apiKey = apiKey;           
            this.secretKey = secretKey;     
            this.accessToken = null; // If this constructor was used, the accessToken is set to null
            this.method = apiMethod;

            // Write to traceLog
            this.appendTrace("apiMethod", apiMethod);
            this.appendTrace("apiKey", apiKey);

            this.dictionaryParams = currDictionaryParams;
            if (this.dictionaryParams == null)
                this.dictionaryParams = new GSDictionary();

            this.useHTTPS = useHTTPS;
        }

        /// <summary>
        /// Constructs a request using an access token that was earlier obtained in the login proccess.
        /// The way to acuire the access token is beyond the scope of this class, but usualy it requires to 
        /// redirect the user to the login url, retrieve the "code" parameter and the exchange it for an access token (in some
        /// cases it is possible to get the access token in one step as a fragment parameter, e.g. www.example.com/callback#access_token=...)
        /// This kind of operation must be done over a secure connection (https).
        /// </summary>
        /// <param name="accessToken">the access token that was earlier obtained in the login proccess</param>        
        /// <param name="apiMethod">The API method (including namespace) to call. For example: socialize.getUserInfo
        /// If namespaces is not supplied "socialize" is assumed</param>
        /// <param name="currDictionaryParams">The request parameters</param>        
        public GSRequest(string accessToken, string apiMethod, GSDictionary currDictionaryParams)
        {
            if (apiMethod == null || apiMethod == "")
                return;

            if (apiMethod.StartsWith("/"))
                apiMethod = apiMethod.Substring("/".Length);

            if (apiMethod.IndexOf(".") == -1)
            {
                this.domain = "socialize.gigya.com";
                this.path = "/socialize." + apiMethod;
            }
            else
            {
                string[] tokens = apiMethod.Split(new char[] { '\\', '.' });
                this.domain = tokens[0] + ".gigya.com";
                this.path = "/" + apiMethod;
            }

            this.apiKey = null;
            this.secretKey = null;
            this.accessToken = accessToken; 
            this.method = apiMethod;
            this.dictionaryParams = currDictionaryParams;
            if (this.dictionaryParams == null)
                this.dictionaryParams = new GSDictionary();

            // Using this constructor requires secure connection
            this.useHTTPS = true;

            // Write to traceLog
            this.appendTrace("apiMethod", apiMethod);
            this.appendTrace("accessToken", accessToken);

        }

        #endregion


        #region Public Methods
       
        /// <summary>
        ///  Associates the specified parameter with the specified value. 
        ///  If the parameter is already exists, the old value is replaced by the specified value.
        ///  If the parameter does not already exist, it will be inserted as a new parameter.
        /// </summary>
        /// <param name="param">parameter name to set</param>
        /// <param name="value">the string value to set for the parameter</param>
        public void setParam(string param, string value)
        {
            this.dictionaryParams.put(param, value);
        }

        /// <summary>
        /// Returns a GSDictionary object containing the parameters of this request.
        /// </summary>
        /// <returns>the params field of this request.</returns>
        public GSDictionary getParams()
        {
            return this.dictionaryParams;
        }

        /// <summary>
        /// Send the request synchronously
        /// </summary>
        /// <returns>a GSResponse object representing Gigya's response</returns>
        public GSResponse send()
        {
            this.format = this.dictionaryParams.getString("format", "json");

            if (string.IsNullOrEmpty(this.method))
                return new GSResponse(this.traceLog, this.method, this.format, 400002); // (Missing required parameter)

            if (string.IsNullOrEmpty(this.accessToken) && (string.IsNullOrEmpty(this.apiKey) || string.IsNullOrEmpty(this.secretKey)))
                return new GSResponse(this.traceLog, this.method, this.format, 400002); // (Missing required parameter)

            string responseStr;
            try
            {
                // Set default params
                this.setParam("format", this.format);
                this.setParam("httpStatusCodes", "false");
                // Append to log
                this.appendTrace("params", this.dictionaryParams);

                // Send request by HTTP POST and store response as string
                responseStr = this.sendRequest("POST");

                // Return the GSResponse object corresponding to the response string
                return new GSResponse(this.traceLog, this.method, responseStr);
            }
            catch (FormatException fex)
            {
                return new GSResponse(this.traceLog, this.method, this.format, 400006, "Invalid parameter value: " + fex.Message);
            }
            catch (Exception ex)
            {
                return new GSResponse(this.traceLog, this.method, this.format, 500000, ex.Message);
            }
        }

        /// <summary>
        ///  Converts a GSDictionary to a query string
        /// </summary>
        /// <param name="addQuestionMark">Set to true if you want the returned string to start with a question mark.</param>
        /// <param name="paramDictionary">the GSDictionary to get the query string from</param>
        /// <returns></returns>
        public static string buildQS(bool addQuestionMark, GSDictionary paramDictionary)
        {
            StringBuilder retQS = new StringBuilder();
            string value;
            if (addQuestionMark)
                retQS.Append("?");

            foreach (string key in paramDictionary.getKeys())
            {
                value = paramDictionary.getString(key, null);
                if (value != null)
                {
                    retQS.Append(key);
                    retQS.Append('=');
                    retQS.Append(GSRequest.UrlEncode(value));
                    retQS.Append('&');
                }
            }

            // Remove the '&' at the end by trimming the end of the StringBuilder
            if (retQS[retQS.Length - 1] == '&')
                retQS.Length--;

            return retQS.ToString();
        }

        /// <summary>
        /// Applies URL encoding rules to the String value, and returns the outcome.
        /// </summary>
        /// <param name="value">the string to encode</param>
        /// <returns>the URL encoded string</returns>
        public static string UrlEncode(string value)
        {
            StringBuilder result = new StringBuilder();
            char[] c = new char[1];

            for (int i = 0; i < value.Length; i++)
            {
                char symbol = value[i];
                if (Array.BinarySearch<char>(unreservedChars, symbol) >= 0)
                    result.Append(symbol);
                else
                {
                    c[0] = symbol;
                    byte[] bytes = Encoding.UTF8.GetBytes(c);
                    foreach (byte b in bytes)
                        result.Append('%' + String.Format("{0:X2}", (int)b));
                }
            }

            return result.ToString();
        }
        
        #endregion


        #region Private Methods

        /// <summary>
        /// Send the actual HTTP/S request
        /// </summary>
        /// <param name="httpMethod">"POST" or "GET"</param>        
        /// <returns></returns>
        private string sendRequest(string httpMethod)
        {
            WebResponse webResponse = null;
            try
            {
                // Set Protocol and URI
                string protocol = this.useHTTPS ? "https" : "http";
                string resourceURI = protocol + "://" + this.domain + this.path;

                if (this.accessToken != null)
                {
                    this.dictionaryParams.put("oauth_token", this.accessToken);
                }
                else // Use apiKey and sign the request using the secretKey
                {
                    // Set Timestamp and Nonce
                    long currTime = SigUtils.CurrentTimeMillis();
                    string timestamp = (currTime / 1000).ToString();
                    string nonce = currTime.ToString();

                    // Set params. DO THIS *BEFORE* CALCULATING THE SIGNATURE 
                    this.dictionaryParams.put("apiKey", apiKey);
                    this.dictionaryParams.put("timestamp", timestamp);
                    this.dictionaryParams.put("nonce", nonce);

                    // Calculate signature. DO THIS ONLY *AFTER* PUTTING ALL OTHER PARAMS IN DICTIONARY
                    string basestring = SigUtils.calcOAuth1Basestring(httpMethod, resourceURI, this.dictionaryParams);
                    string signature = SigUtils.calcSignature(basestring, this.secretKey);
                    this.dictionaryParams.put("sig", signature);
                }

                // Build the query string from the dictionary
                string data = GSRequest.buildQS(false /*dont add question mark*/, this.dictionaryParams);

                this.appendTrace("URL", resourceURI);
                this.appendTrace("postData", data);

                // Create HttpRequset
                HttpWebRequest request = (HttpWebRequest)WebRequest.Create(resourceURI);
                request.ContentType = "application/x-www-form-urlencoded";
                request.Method = httpMethod;
                byte[] buffer = Encoding.UTF8.GetBytes(data);
                request.ContentLength = buffer.Length;
                // Write content to the request
                using (Stream stream = request.GetRequestStream())
                {                    
                    stream.Write(buffer, 0, buffer.Length);
                    stream.Close();
                }

                // Get HttpResponse
                webResponse = request.GetResponse();
                this.appendTrace("server", webResponse.Headers["x-server"]);
                string respText = string.Empty;
                using (StreamReader sr = new StreamReader(webResponse.GetResponseStream()))
                {
                    respText = sr.ReadToEnd();
                    sr.Close();
                }

                this.appendTrace("response", respText);
                return respText;
            }
            catch (Exception e)
            {
                throw e;
            }
            finally
            {
                if (webResponse != null)
                {
                    try
                    {
                        webResponse.Close();
                    }
                    catch (Exception)
                    {
                    }
                }
            }
        }

        private void appendTrace(string name, object value)
        {
            this.traceLog.Append(string.Format("{0}={1}\r\n", name, value));
        }               
        #endregion
    }
}
